JAVA安全 | 您所在的位置:网站首页 › maven 打jar包配置 -csdn › JAVA安全 |
0x00 前言 字节码增强技术是一类对现有字节码进行修改或者动态生成全新字节码文件的技术,它在网络安全领域中的作用之一就是用来以“零侵入“方式插入恶意字节码,达到权限维持和RCE的目的,故而我更愿意叫它字节码插桩技术。 0x01 ASM&&JDPA&&JavaAgent介绍一、ASMASM是一个字节码操作框架,它直接从字节码的层面来修改现有类或动态生成类,官网地址:https://asm.ow2.io。使用ASM需要了解字节码文件结构与JVM指令,并理解访问者设计模式。 二、JPDA要介绍JavaAgent追根溯源就得先介绍JPDA。 JPDA英文全称为Java Platform Debugger Architecture,即Java平台调试体系。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。 正如JPDA名称中的Debugger,这个体系为开发人员提供了一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议。本质上说,它是我们通向虚拟机,考察虚拟机运行态的一个通道,一套工具。另外,我们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准。 JPDA定义了一整套完整的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI): 这三个模块把调试过程分解成三个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。 1.JDIJDI英文全称Java Debug Interface,即 Java 调试接口,由Java语言实现,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行。 2.JDWPJDWP英文全称Java Debug Wire Protocol,即Java 调试线协议,是一个为 Java 调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。 3.JVMTI这是本文中需要重点认识的。 JVMTI英文全称Java Virtual Machine Tool Interface,即 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。 通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。JVMTI 的前身是 JVMDI(Java Virtual Machine Debug Interface) 和 JVMPI(Java Virtual Machine Profiler Interface),它们原来分别被用于提供调试 Java 程序以及 Java 程序调节性能的功能。 JVMTI就是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。 有关JPDA的更详细内容可以参考这篇文章: JPDA 体系概览 三、JavaAgent1.JavaAgent本质Agent就是JVMTI的一种实现,它有两种启动方式: 一、随Java进程启动而启动,经常见到的java -agentlib就是这种方式; 二、运行时载入,通过Attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内 什么又是Attach API(附加应用程序接口)呢? Attach API是JVMTI的一种机制,其作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。 2.JavaAgent "表象"JavaAgent的表象就是java命令的一个参数,该参数可以用于指定一个jar包,jar包内至少含有一个遵循一组严格约定的常规Java类,对该jar包有两个要求: 1.这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。 2.Premain-Class 指定的那个类必须实现 premain() 方法。premain即运行在main函数之前,当JVM启动时,在执行main函数之前,会先运行-javaagent所指定jar包内Premain-Class类的premain方法。 我们可以通过在命令行输入java看到与java agent相关的参数: 在-javaagent中提到了java.lang.instrument,它是在rt.jar中定义的一个包: 这个包提供了允许Java编程语言代理程序检测在JVM上运行的程序的服务,检测机制是修改方法的字节码。也可以理解为它提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。故而可以将JavaAgent看作一个Class 类型的转换器,其中对Class类型进行修改重要的是Instrumentation和ClassFileTransformer接口。 javaagent与instrument的关系就是javaagent的实现使用到了java.lang.instrument软件包。 四、Instrumetation和ClassFileTransformer接口Instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持,它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。 下面就介绍下java.lang.instrument包中两个重要的接口: 1.ClassFileTransformer要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。 该接口定义如下: public interface ClassFileTransformer { //transform()方法会在类文件被加载时调用,而在transform方法里,可以利用ASM、Javassist等对传入的字节码进行改写或替换,生成新的字节码数组后返回。 byte[] transform( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; } 2.Instrumentation通过Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。 该接口的定义和重要方法如下: public interface Instrumentation { //增加一个Class文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。 void addTransformer(ClassFileTransformer transformer); //删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。 void retransformClasses(Class... classes) throws UnmodifiableClassException; } 0x02 使用JavaAgent进行字节码插桩一、JVM启动时进行插桩1.premain方法要实现在JVM启动时进行插桩,即随JavaAgent随Java进程启动而启动,那么在javaagent命令中指定的jar包的Premain-class类中必须要有premain()方法。 premain方法有两种格式: public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)JVM 会优先加载带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在rt.jar包中的sun.instrument.InstrumentationImpl 类的loadClassAndStartAgent()方法中: 我们如何来写个简单代码来实现下呢? 2.代码实现首先,我们建一个普通java工程,工程名为Test,有一个类Test01如下: public class Test01 { public static void main(String[] args) { System.out.println("test"); } }我们想通过插桩,使得在打印test的前后打印start和end,且修改字节码部分通过ASM来实现。 新建一个Maven项目Enhancement01,自写适配器代码如下: import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import static org.objectweb.asm.Opcodes.*; public class Adapter extends ClassVisitor { public Adapter(ClassVisitor cv){ super(ASM5,cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions); if(!name.equals("") && mv != null){ mv = new MyMethodVisitor(mv); } return mv; } class MyMethodVisitor extends MethodVisitor{ public MyMethodVisitor(MethodVisitor mv){ super(ASM5,mv); } @Override public void visitCode() { //在方法访问前打印start super.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("start"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } @Override public void visitInsn(int opcode) { //在方法返回前打印end if((opcode >= IRETURN && opcode 4.0.0 ByteCode Enhancement 1.0-SNAPSHOT org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 org.apache.maven.plugins maven-assembly-plugin 2.4 false Premain_Agent jar-with-dependencies true PremainAgent true true make-assembly package assembly org.ow2.asm asm 9.2打包时会将项目中的所有依赖都打进jar包,且会在jar包为我们生成包含如下内容的MANIFEST.MF文件: Premain-Class: PremainAgent Can-Redefine-Classes: true Can-Retransform-Classes: true通过Maven-->Lifecycle-->clean-->package可得到jar包: 4.JVM启动时运行javaagent有两种方式: (1)在IDEA中运行Test01时,在VM options中指定-javaagent: (2)在cmd中运行: 运行完成会生成一个Test01_1.class文件,通过IDEA 打开: Perfect!!!该class即为成功插桩后加载进入jvm运行的字节码,是不是帅到飞起~ 二、JVM运行中进行插桩上面的实验只是在java程序启动时才能进行插桩,实战意义并不大,那么如何对一个正在运行中的java进程进行插桩呢?这是在JDK1.6之后才能进行的操作。 1.agentmain方法在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类。 agentmain也有两种方式: //采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调 public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)同样,带Instrumentation参数的方法比不带优先级更高。开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。 2.Attach API及注入原理在Java6 以后实现启动后加载的新实现是Attach api,有 2 个主要的类VirtualMachine和VirtualMachineDescriptor,都在 tools.jar中的com.sun.tools.attach 包里: 注:tools.jar包默认是没有载入到项目的JDK中的,因为该包位于JDK安装目录下的/lib中,我们需要通过 file-->project structer-->SDKs将/lib/tools.jar加入到ClassPath中: 下面分别介绍下VirtualMachine和VirtualMachineDescriptor: (1)VirtualMachine类 该类提供了获取系统信息比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等)的方法,以及 loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。 该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。 (2)VirtualMachineDescriptor类 一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。 Attach实现动态注入原理 通过VirtualMachine类的attach(pid)方法,attach到一个运行中的java进程上,之后通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。 3.代码实现首先,我们在Test工程中写一个Test02类,该类将作为我们的运行中的java程序: public class Test02 { public static void main(String[] args) { while (true) { try { Thread.sleep(5000L); } catch (Exception e) { break; } process(); } } public static void process(){ System.out.println("process..."); } }我们希望在process方法中打印process后弹出计算器,即我们希望插桩成功后process方法的代码是这样的: public static void process(){ System.out.println("process..."); try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } }那么如何使用ASM写出对应的代码呢?这里用到一个ASM ByteCode Outline的插件,在IDEA中通过setting-->plugins进行搜索下载安装即可。 安装后我们编写一个Test03.java,其process方法如上所示,将其编译,然后右键-->show bytecode outline,在ASMified模块即可看到用ASM生成该方法的代码: 新建一个maven项目Enhancement02,自写适配器如下: import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import static org.objectweb.asm.Opcodes.*; public class Adapter extends ClassVisitor { public Adapter(ClassVisitor cv){ super(ASM5,cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions); if(name.equals("process")){ mv = new MyMethodVisitor(mv); } return mv; } class MyMethodVisitor extends MethodVisitor{ public MyMethodVisitor(MethodVisitor mv){ super(ASM5,mv); } @Override public void visitInsn(int opcode) { //在方法返回前弹计算器 if((opcode >= IRETURN && opcode |
CopyRight 2018-2019 实验室设备网 版权所有 |